﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using Red.Controls.Diagrams.Declarators;
using Red.Controls.Diagrams.InteropUtils;
using Red.Core;
using Utils;

namespace Red.Controls.Diagrams.ViewModels
{
    /// <summary>
    /// Defines a node in the view-model.
    /// Nodes are connected to other nodes through attached connectors (aka anchor/connection points).
    /// </summary>
    public class NodeViewModel : ViewModel
    {
        #region Data

        private double _x;
        private double _y;

        private string _name;

        /// <summary>
        /// The Z index of the node.
        /// </summary>
        private int _zIndex;

        /// <summary>
        /// The size of the node.
        /// 
        /// Important Note: 
        ///     The size of a node in the UI is not determined by this property!!
        ///     Instead the size of a node in the UI is determined by the data-template for the Node class.
        ///     When the size is computed via the UI it is then pushed into the view-model
        ///     so that our application code has access to the size of a node.
        /// </summary>
        private Size _size = Size.Empty;

        /// <summary>
        /// List of input connectors (connections points) attached to the node.
        /// </summary>
        private ImpObservableCollection<ConnectorViewModel> _inputConnectors;

        /// <summary>
        /// List of output connectors (connections points) attached to the node.
        /// </summary>
        private ImpObservableCollection<ConnectorViewModel> _outputConnectors;

        /// <summary>
        /// Set to 'true' when the node is selected.
        /// </summary>
        private bool _isSelected;

        private Color _background;
        
        private IObject _iObject;

        private readonly NodeDeclarators _declarators;

        protected NodeDeclarator _nodeDeclarator;

        #endregion Data

        #region C-tor

        public NodeViewModel()
        {
        }

        public NodeViewModel( IObject iObject, NodeDeclarators declarators )
        {
            Object = iObject;
            _declarators = declarators;
        }

        #endregion C-tor

        #region Properties

        /// <summary>
        /// Reference to diagram model that contains current node.
        /// It's used to access diagram's scale property to handle level of detail of this node.
        /// </summary>
        public DiagramViewModel DiagramViewModel { get; set; }

        /// <summary>
        /// Underlying object that holds data (from definitions or from RTTI, depends on the implementation).
        /// All properties that are changed via NodeViewModel are also changed in internal data structures.
        /// </summary>
        public IObject Object
        {
            get { return _iObject; }
            set 
            {
                _iObject = value;
                if ( _iObject != null )
                {
                    X = double.Parse( Object.ReadProperty( "X" ) );
                    Y = double.Parse( Object.ReadProperty( "Y" ) );
                }
            }
        }

        /// <summary>
        /// The name of the node.
        /// </summary>
        public string Name 
        {
            get { return _name; }
            private set
            {
                if ( _name == value )
                    return;

                _name = value;
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// Class of the node.
        /// </summary>
        public string Class => _nodeDeclarator.Class;

        public bool IsContainer => _nodeDeclarator.IsContainer;

        /// <summary>
        /// The X coordinate for the position of the node.
        /// </summary>
        public virtual double X
        {
            get { return _x; }
            set
            {
                if ( _x == value )
                    return;

                _x = value;
                OnPositionChanged();
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// The Y coordinate for the position of the node.
        /// </summary>
        public virtual double Y
        {
            get { return _y; }
            set
            {
                if ( _y == value )
                    return;

                _y = value;
                OnPositionChanged();
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// The Z index of the node.
        /// </summary>
        public int ZIndex
        {
            get
            {
                return _zIndex;
            }
            set
            {
                if ( _zIndex == value )
                    return;

                _zIndex = value;
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// The size of the node.
        /// 
        /// Important Note: 
        ///     The size of a node in the UI is not determined by this property!!
        ///     Instead the size of a node in the UI is determined by the data-template for the Node class.
        ///     When the size is computed via the UI it is then pushed into the view-model
        ///     so that our application code has access to the size of a node.
        /// </summary>
        public Size Size
        {
            get
            {
                return _size;
            }
            set
            {
                if ( _size == value )
                    return;

                _size = value;
                OnSizeChanged();
            }
        }

        public Color Background
        {
            get { return _background; }
            set
            {
                if ( _background == value )
                    return;

                _background = value;
                OnPropertyChanged();
            }
        }

        public virtual string Comment
        {
            get { return Object.ReadProperty( "Comment" ); }
            set
            {
                var currentValue = Object.ReadProperty( "Comment" );
                if ( currentValue == value )
                    return;

                Object.WriteProperty( "Comment", value );
                OnPropertyChanged();
            }
        }

        public NodeVisualType VisualType { get { return _nodeDeclarator.VisualType; } }

        /// <summary>
        /// List of input connectors (connections points) attached to the node.
        /// </summary>
        public ImpObservableCollection<ConnectorViewModel> InputConnectors
        {
            get
            {
                if ( _inputConnectors == null )
                {
                    _inputConnectors = new ImpObservableCollection<ConnectorViewModel>();
                    _inputConnectors.ItemsAdded += inputConnectors_ItemsAdded;
                    _inputConnectors.ItemsRemoved += inputConnectors_ItemsRemoved;
                }

                return _inputConnectors;
            }
        }

        /// <summary>
        /// List of output connectors (connections points) attached to the node.
        /// </summary>
        public ImpObservableCollection<ConnectorViewModel> OutputConnectors
        {
            get
            {
                if ( _outputConnectors == null )
                {
                    _outputConnectors = new ImpObservableCollection<ConnectorViewModel>();
                    _outputConnectors.ItemsAdded += outputConnectors_ItemsAdded;
                    _outputConnectors.ItemsRemoved += outputConnectors_ItemsRemoved;
                }

                return _outputConnectors;
            }
        }

        /// <summary>
        /// A helper property that retrieves a list (a new list each time) of all connections attached to the node. 
        /// </summary>
        public ICollection<ConnectionViewModel> AttachedConnections
        {
            get
            {
                var attachedConnections = new List<ConnectionViewModel>();

                foreach ( var connector in InputConnectors )
                    attachedConnections.AddRange( connector.AttachedConnections );

                foreach ( var connector in OutputConnectors )
                    attachedConnections.AddRange( connector.AttachedConnections );

                return attachedConnections;
            }
        }

        /// <summary>
        /// Set to 'true' when the node is selected.
        /// </summary>
        public bool IsSelected
        {
            get
            {
                return _isSelected;
            }
            set
            {
                if ( _isSelected == value )
                    return;

                _isSelected = value;
                OnPropertyChanged();
            }
        }

        public string ID { get { return Object.ID; } }

        #endregion Properties

        #region Events

        /// <summary>
        /// Event raised when the size of the node is changed.
        /// The size will change when the UI has determined its size based on the contents
        /// of the nodes data-template.  It then pushes the size through to the view-model
        /// and this 'SizeChanged' event occurs.
        /// </summary>
        public event EventHandler<EventArgs> SizeChanged;
        private void OnSizeChanged()
        {
            if ( SizeChanged != null )
                SizeChanged( this, EventArgs.Empty );
        }

        public event EventHandler<EventArgs> PositionChanged;
        private void OnPositionChanged()
        {
            if ( PositionChanged != null )
                PositionChanged( this, EventArgs.Empty );
        }

        #endregion Events

        #region Event handlers

        /// <summary>
        /// Event raised when connectors are added to the node.
        /// </summary>
        private void inputConnectors_ItemsAdded( object sender, CollectionItemsChangedEventArgs e )
        {
            foreach ( ConnectorViewModel connector in e.Items )
            {
                connector.ParentNode = this;
                connector.Type = ConnectorType.Input;
            }
        }

        /// <summary>
        /// Event raised when connectors are removed from the node.
        /// </summary>
        private void inputConnectors_ItemsRemoved( object sender, CollectionItemsChangedEventArgs e )
        {
            foreach ( ConnectorViewModel connector in e.Items )
            {
                connector.ParentNode = null;
                connector.Type = ConnectorType.Undefined;
            }
        }

        /// <summary>
        /// Event raised when connectors are added to the node.
        /// </summary>
        private void outputConnectors_ItemsAdded( object sender, CollectionItemsChangedEventArgs e )
        {
            foreach ( ConnectorViewModel connector in e.Items )
            {
                connector.ParentNode = this;
                connector.Type = ConnectorType.Output;
            }
        }

        /// <summary>
        /// Event raised when connectors are removed from the node.
        /// </summary>
        private void outputConnectors_ItemsRemoved( object sender, CollectionItemsChangedEventArgs e )
        {
            foreach ( ConnectorViewModel connector in e.Items )
            {
                connector.ParentNode = null;
                connector.Type = ConnectorType.Undefined;
            }
        }

        #endregion Event handlers

        #region Internal methods

        /// <summary>
        /// Does this node accept given data drag?
        /// </summary>
        /// <param name="obj">Dragged data.</param>
        /// <returns>True if yes, false if no.</returns>
        internal virtual bool AcceptsDraggedData( IDataObject obj )
        {
            foreach ( var dropDec in _nodeDeclarator.Drops )
            {
                if ( obj.GetDataPresent( dropDec.Class ) )
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// On drop dragged data.
        /// </summary>
        /// <param name="obj">Dragged data.</param>
        internal virtual void DropDraggedData( IDataObject obj )
        {
            foreach ( var dropDesc in _nodeDeclarator.Drops )
            {
                if ( obj.GetDataPresent( dropDesc.Class ) )
                {
                    _iObject.WritePropertyDirect( dropDesc.Binding, (string)obj.GetData( dropDesc.Class ) );
                }
            }
        }

        internal void InitFromNodeDeclarator( NodeDeclarator nodeDeclarator )
        {
            _nodeDeclarator = nodeDeclarator;
            Name = nodeDeclarator.Title;
            Background = nodeDeclarator.Color;
            InputConnectors.Clear();
            OutputConnectors.Clear();
            var thisInputs = nodeDeclarator.InputsInternal;
            var inheritedInputs = nodeDeclarator.Inputs.Except( thisInputs );

            var thisOutputs = nodeDeclarator.OutputsInternal;
            var inheritedOutputs = nodeDeclarator.Outputs.Except( thisOutputs );

            InputConnectors.AddRange( inheritedInputs.Select( input => CreateInputConnectorViewModel( input, true ) ) );
            OutputConnectors.AddRange( inheritedOutputs.Select( output => CreateOutputConnectorViewModel( output, true ) ) );

            InputConnectors.AddRange( thisInputs.Select( input => CreateInputConnectorViewModel( input ) ) );
            OutputConnectors.AddRange( thisOutputs.Select( output => CreateOutputConnectorViewModel( output ) ) );
        }

        #endregion Internal methods

        #region Protected methods

        protected virtual ConnectorViewModel CreateInputConnectorViewModel( NodeConnectorDeclarator connectorDeclarator, bool isInherited = false )
        {
            return new ConnectorViewModel( connectorDeclarator.Name, Object.ID, new InputProperty( Object, connectorDeclarator.Name ), isInherited, Background );
        }

        protected virtual ConnectorViewModel CreateOutputConnectorViewModel( NodeConnectorDeclarator connectorDeclarator, bool isInherited = false )
        {
            var property = Object.GetProperty( connectorDeclarator.Binding );

            if( property == null )
                return new ConnectorViewModel( connectorDeclarator.Name + (" (Invalid Binding)"), null, null, isInherited, ( Color )ColorConverter.ConvertFromString( "#FFAA0000" ), false );

            var propertyDeclarator = _declarators.GetNodeDeclarator( property.GetType() );
            var uniqueColors = GetCompatibleDeclaratorsWithUniqueColor( propertyDeclarator );
            var classUsage = uniqueColors.Count();

            Color connectorColor;
            if ( classUsage == 1 )
            {
                // we have exact match
                // all blocks that can be connected to this connector have the same color
                connectorColor = propertyDeclarator.Color;
            }
            else
            {
                // blocks that can be connected to this connector have multiple colors
                // use neutral color instead
                connectorColor = ( Color )ColorConverter.ConvertFromString( "#FFA7A7F6" );
            }

            return new ConnectorViewModel( connectorDeclarator.Name, property.ID, property, isInherited, connectorColor );
        }

        #endregion Protected methods

        #region Private methods

        private IEnumerable<NodeDeclarator> GetCompatibleDeclaratorsWithUniqueColor( NodeDeclarator declarator )
        {
            return _declarators.GetIheritedFrom( declarator ).GroupBy( item => item.Color ).Select( item => item.First() );
        }

        #endregion Private methods
    }
}
